#!/bin/perl 

## ----------------------------------------------------
## Title :			SYSLOG to Palo Alto UserID
## Description :	Parse log file and extract informations about User and their IP
## Date :			2013-07-13
## Version :		0.2
## Author :			@TiTom
## ----------------------------------------------------

## ----------------------------------------------------
## Todo List
## ----------------------------------------------------
## - Add switch to manage CLI parameters
## ----------------------------------------------------
=head1 NAME

Syslog to Palo Alto USER ID

=head1 DESCRIPTION

Monitor any file to parse log entries and identify user login / logout and send userid information to Palo Alto user id engine.

=head1 Author

Nomios Technical Team
@TiTom73(dev@inetsix.net)

=head1 SYNOPSYS

Usage is :
	
	perl file.pl
	
It is mandatory to use configuration file to provide required informations such as :

- user ID server IP

- Domain

Regexp could be manage in the file regexp.dict

=head1 DEPENDENCIES

This module is based on :

- strict

- warnings

- POSIX

- PAN::API : Requirement : libio-socket-ssl-perl / As PAN::API must be changed from original Palo Alto module, do not change it.

- File::Tail

- Switch

- Config::IniFiles

=cut
########################################################
#	Requirements
########################################################
## General Libs
use strict;
use warnings; 

## PAN::API has been changed to disable CERT Check on IO::SOCKET::SSL
use PAN::API;	## Requirement : libio-socket-ssl-perl

## CPAN Library
use File::Tail;	
use Switch;
use Config::IniFiles;

########################################################
#	Variables
########################################################
## Script Version
=head VERSION
VERSION = 0.2
=cut
our $VERSION = 0.2;
## Config File
our $CONFIG = "./config.cfg";
## load Configuration file
our $cfg = Config::IniFiles->new (
	-file => $CONFIG
);
## Load from Configuration File
## Palo Alto IP Adress
our $SERVER = &conf_get_value("general","userid_engine");
## Domain (Not used yet)
our $DOMAIN = &conf_get_value("general","user_domain");
## Log file
my $LOG = &conf_get_value("general","monitor_file");
## Regexp File
our $REGEXP = &conf_get_value("general","regexp_file");

## Static Parameters
## Array to store User => IP
my %CONNECTION;
## Array to identify CONNECTION Message
our @DICT_CONNECT=('connect','start');
## Array to identify DISCONNECTION Message
our @DICT_DISCONNECT=('disconnect','stop','end');

## Specific Constant
use constant TRUE => 1;
use constant FALSE => 0;

## Debug Specific
## Manage by Specific value in Config File
our $DEBUG =  &conf_get_value("general","DEBUG");

########################################################
#	Functions
########################################################
=head1 METHODS

=head2 information

Display Script Banner

=cut
sub informations {
	print "Syslog to Palo Alto UserID - Powered by Nomios - version $VERSION\n";
	printf("\n
    _   __                _               __
   / | / /___  ____ ___  (_)___  _____   / /
  /  |/ / __ \\/ __ `__ \\/ / __ \\/ ___/  / / 
 / /|  / /_/ / / / / / / / /_/ (__  )  /_/  
/_/ |_/\\____/_/ /_/ /_/_/\\____/____/  (_)   
\n
Not because we can, but because we do it better !\n\n");
}

## ----------------------------------------------------
## Function to add user to PA UserID.
## Provide USER and IP informations to work.
=head2 pa_add($user,$ip)

Function to add user to PA UserID.
Provide USER and IP informations to work.

=cut
sub pa_add {
	my ($user,$ip)=@_;

	## Send informations to Palo Alto 
	## If SSL Issue, modify PAN::API to include SSL_verify_mode => SSL_VERIFY_NONE on IO::Socket::SSL->new
	## Communication is based on default port : 5006 for XML User-ID API. Must be enable on your User-ID Server
	my $uid=PAN::API::UID->new($SERVER);
	
	## Must validate how to format user informations
	$uid->add('login',$user,$ip);
	if( $DEBUG == TRUE ) { print "uid->add(\'login\',$user,$ip)\n"}
	if( $DEBUG == FALSE ) { $uid->submit() }
	print "\n";
}

## ----------------------------------------------------
## Function to search value in Configuration File.
=head2 conf_get_value($section,$id)

Function to search value in Configuration File.

=cut
sub conf_get_value {
	## Get position and key to search
	my ($section,$id)=@_;
	## Check if value exist
	if( $cfg->exists($section,$id)){
		## If yes, then return it
		return $cfg->val($section,$id);
	}
	else {
		## Otherwise return 0
		return FALSE;
	}
}

## ----------------------------------------------------
## Function to delete user to PA UserID.
## Provide USER and IP informations to work.
=head2 pa_del($user,$ip)

Function to delete user to PA UserID.
Provide USER and IP informations to work.

=cut
sub pa_del {
	my ($user,$ip)=@_;

	## Send informations to Palo Alto 
	## If SSL Issue, modify PAN::API to include SSL_verify_mode => SSL_VERIFY_NONE on IO::Socket::SSL->new
	## Communication is based on default port : 5006 for XML User-ID API. Must be enable on your User-ID Server
	my $uid=PAN::API::UID->new($SERVER);
	
	## Must validate how to format user informations
	$uid->add('logout',$user,$ip);
	if( $DEBUG == TRUE ) { print "uid->add(\'logout\',$user,$ip)\n"}
	if( $DEBUG == FALSE ) { $uid->submit() }
	print "\n";
}

## ----------------------------------------------------
## Check if REGEXP contains mandatory statement
## REGEXP must contains one identifier for user (\w+) and another one for IP ( (\d+\.\d+\.\d+\.\d+) )
=head2 regexp_checker($exp)

Check if REGEXP contains mandatory statement
REGEXP must contains one identifier for user (\w+) and another one for IP ( (\d+\.\d+\.\d+\.\d+) )

=cut
sub regexp_checker {
	my ($exp) = @_;
	## Must be changed to support multiform Regexp
	## Deploy specific Array with all Regex Form
	if( $exp =~ m/.*\(\\w\+\).*/ )
	#and $exp =~ /\(\\d\+\\\.\\d\+\\\.\\d\+\\\.\\d\+\)/ )
	{ return 1 }
	return 0;
}

## ----------------------------------------------------
## Function to parse log and identify USER and IP informations
## Generic function / Must specify how to manage connect or disconnect action
=head2 parser_userid($log)

Function to parse log and identify USER and IP informations
Generic function / Must specify how to manage connect or disconnect action

=cut
sub parser_userid {
	my ($log) = @_;
	my @USER = undef;
	## Load Regexp from file
	open FREG, $REGEXP or die $!;

	## Check log message against all regexp from FREG file
	while(my $exp = <FREG> )
	{
		## If line is not empty
		if( $exp =~ m/^(?!$)/) {
			## Clean line to remove \n at the end
			if($exp=~m/\n$/) { chomp($exp) }

			## Debug Log
			
			## Check if REGEXP is consistent
			## ie : REGEXP must have 2 specific match to identifiy USER and IP
			## If Regexp is OK, then we can use it against our log
			if( &regexp_checker($exp) == 1 ) {
					
				## Construct Regular Expression
				my $reger = qr/.*$exp.*/;
				
				## If $log match regexp, retrieve USER and IP informations
				if( $log =~ $reger) {
					push(@USER,$1);
					push(@USER,$2);
					last;
				}
			}
		}
	}
	close (FREG);
	return @USER;
}

## ----------------------------------------------------
## Function to parse log and identify if it is CONNECT
=head2 msgtype_connect($log)

Function to parse log and identify if it is CONNECT

=cut
sub msgtype_connect {
	my ($log) = @_;
	foreach my $exp (@DICT_CONNECT)
	{
		my $reger = qr/.*$exp.*/;
		if( $log =~ $reger ) { 
			return TRUE;
		}
	}
	return FALSE;
}

## ----------------------------------------------------
## Function to parse log and identify if it is DISCONNECT
=head2 msgtype_disconnect($log)

Function to parse log and identify if it is DISCONNECT

=cut
sub msgtype_disconnect {
	my ($log) = @_;
	foreach my $exp (@DICT_DISCONNECT)
	{
		my $reger = qr/.*$exp.*/;
		if( $log =~ $reger ) { 
			return TRUE;
		}
	}
	return FALSE;
}


## ----------------------------------------------------
## Function to parse log and identify if it is CONNECT or DISCONNECT
=head2 parser_msgtype($log)

Function to parse log and identify if it is CONNECT or DISCONNECT

=cut
sub parser_msgtype {
	my ($log) = @_;
	if( &msgtype_disconnect($log) == TRUE) { return "DISCONNECT" }
	elsif ( &msgtype_connect($log) == TRUE) { return "CONNECT" }
}

## ----------------------------------------------------
## Function to add new user to our Connection Manager and send login to Palo Alto
## Use with &user_add(USER,IP)
=head2 user_add(@user)

Function to add new user to our Connection Manager and send login to Palo Alto
Use with &user_add(USER,IP)

=cut
sub user_add {
	my @user = @_;
	print "- Add user $user[1] with IP $user[2] to our session manager\n";
	
	## Add user with IP information in our Connection Manager
	## Mandatory if logout message does not contain IP information
	$CONNECTION{$user[1]} = $user[2];

	## Send USERID to Palo Alto Device
	&pa_add($user[1],$user[2]);
	return;
}

## ----------------------------------------------------
## Function to delete user from our Connection Manager and send logout to Palo Alto
## Use with &user_del(USER)
=head2 user_add($user)

Function to delete user from our Connection Manager and send logout to Palo Alto
Use with &user_del(USER)

=cut
sub user_del {
	my ($user) = @_;
	
	## Display Informations to your default STDIN
	print "- Remove user $user ";
	if( defined($CONNECTION{$user}) ) { print "($CONNECTION{$user}) " }
	print "from our session manager\n";
	
	## If IP is not defined, User has not been correctly identify and not pushed to PA with this script
	## Nothing to do as PA:API require client IP
	if( not defined($CONNECTION{$user}) ) { return }

	## Retrieve IP information for specific user
	my $ip = $CONNECTION{$user};
	
	## remove entry in our Connection Manager
	delete $CONNECTION{$user};

	## Send USERID to Palo Alto Device
	&pa_del($user,$ip);
	
	return;
}

########################################################
#	Main Section
########################################################
## Display Banner at startup
&informations();


## Define File to monitor
my $file=File::Tail->new(name=>$LOG, interval=>2, maxinterval=>10);
printf( "--------------------------------------------------------\n");
printf( "Loading configuration from %s\n",$CONFIG);
printf( "Loading file %s\n",$LOG);
printf( "Loading file %s\n",$REGEXP);
printf( "--------------------------------------------------------\n\n");


while (my $line=$file->read) {	
	my @result = &parser_userid($line);
	my $ACTION = &parser_msgtype($line);

	## print "New Log : $line";

	## If Message is a DISCONNECT one
	## We remove it from our connection manager
	if( $ACTION =~ m/^DISCONNECT$/ ) {
		print "- New $ACTION message for user <<$result[1]>>\n";
		&user_del($result[1]);
		print "\n";
	}
	## Else If it is a CONNECT Message
	## We add user to our connection manager
	elsif( $ACTION =~ m/^CONNECT$/ and defined($result[1]) and defined $result[2]) { 
		print "- New connection for user <<$result[1]>> with IP $result[2]\n"; 
		&user_add(@result);
		print "\n";
	}
}
